#
# CORE
# Copyright (c)2010-2014 the Boeing Company.
# See the LICENSE file included in this distribution.
#
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
#
'''
nodes.py: definition of an EmaneNode class for implementing configuration
control of an EMANE emulation. An EmaneNode has several attached NEMs that
share the same MAC+PHY model.
'''

import sys
import os.path

from core.api import coreapi
from core.coreobj import PyCoreNet
try:
    from emanesh.events import EventService
    from emanesh.events import LocationEvent
except Exception, e:
    pass

try:
    import emaneeventservice
    import emaneeventlocation
except Exception, e:
    ''' Don't require all CORE users to have EMANE libeventservice and its
        Python bindings installed.
    '''
    pass

class EmaneNet(PyCoreNet):
    ''' EMANE network base class.
    '''
    apitype = coreapi.CORE_NODE_EMANE
    linktype = coreapi.CORE_LINK_WIRELESS
    type = "wlan" # icon used

class EmaneNode(EmaneNet):
    ''' EMANE node contains NEM configuration and causes connected nodes
        to have TAP interfaces (instead of VEth). These are managed by the
        Emane controller object that exists in a session.
    '''
    def __init__(self, session, objid = None, name = None, verbose = False,
                start = True):
        PyCoreNet.__init__(self, session, objid, name, verbose, start)
        self.verbose = verbose
        self.conf = ""
        self.up = False
        self.nemidmap = {}
        self.model = None
        self.mobility = None

    def linkconfig(self, netif, bw = None, delay = None,
                   loss = None, duplicate = None, jitter = None, netif2 = None):
        ''' The CommEffect model supports link configuration.
        '''
        if not self.model:
            return
        return self.model.linkconfig(netif=netif, bw=bw, delay=delay, loss=loss,
                            duplicate=duplicate, jitter=jitter, netif2=netif2)

    def config(self, conf):
        #print "emane", self.name, "got config:", conf
        self.conf = conf

    def shutdown(self):
        pass

    def link(self, netif1, netif2):
        pass

    def unlink(self, netif1, netif2):
        pass

    def setmodel(self, model, config):
        ''' set the EmaneModel associated with this node
        '''
        if (self.verbose):
            self.info("adding model %s" % model._name)
        if model._type == coreapi.CORE_TLV_REG_WIRELESS:
            # EmaneModel really uses values from ConfigurableManager
            #  when buildnemxml() is called, not during init()
            self.model = model(session=self.session, objid=self.objid,
                               verbose=self.verbose)
        elif model._type == coreapi.CORE_TLV_REG_MOBILITY:
            self.mobility = model(session=self.session, objid=self.objid,
                               verbose=self.verbose, values=config)

    def setnemid(self, netif, nemid):
        ''' Record an interface to numerical ID mapping. The Emane controller
            object manages and assigns these IDs for all NEMs.
        '''
        self.nemidmap[netif] = nemid

    def getnemid(self, netif):
        ''' Given an interface, return its numerical ID.
        '''
        if netif not in self.nemidmap:
            return None
        else:
            return self.nemidmap[netif]

    def getnemnetif(self, nemid):
        ''' Given a numerical NEM ID, return its interface. This returns the
            first interface that matches the given NEM ID.
        '''
        for netif in self.nemidmap:
            if self.nemidmap[netif] == nemid:
                return netif
        return None

    def netifs(self, sort=True):
        ''' Retrieve list of linked interfaces sorted by node number.
        '''
        return sorted(self._netif.values(), key=lambda ifc: ifc.node.objid)

    def buildplatformxmlentry(self, doc):
        ''' Return a dictionary of XML elements describing the NEMs
            connected to this EmaneNode for inclusion in the platform.xml file.
        '''
        ret = {}
        if self.model is None:
            self.info("warning: EmaneNode %s has no associated model" % \
                      self.name)
            return ret
        for netif in self.netifs():
            # <nem name="NODE-001" definition="rfpipenem.xml">
            nementry = self.model.buildplatformxmlnementry(doc, self, netif)
            # <transport definition="transvirtual.xml" group="1">
            #    <param name="device" value="n1.0.158" />
            # </transport>
            trans = self.model.buildplatformxmltransportentry(doc, self, netif)
            nementry.appendChild(trans)
            ret[netif] = nementry

        return ret

    def buildnemxmlfiles(self, emane):
        ''' Let the configured model build the necessary nem, mac, and phy
            XMLs.
        '''
        if self.model is None:
            return
        # build XML for overall network (EmaneNode) configs
        self.model.buildnemxmlfiles(emane, ifc=None)
        # build XML for specific interface (NEM) configs
        need_virtual = False
        need_raw = False
        vtype = "virtual"
        rtype = "raw"
        for netif in self.netifs():
            self.model.buildnemxmlfiles(emane, netif)
            if "virtual" in netif.transport_type:
                need_virtual = True
                vtype = netif.transport_type
            else:
                need_raw = True
                rtype = netif.transport_type
        # build transport XML files depending on type of interfaces involved
        if need_virtual:
            self.buildtransportxml(emane, vtype)
        if need_raw:
            self.buildtransportxml(emane, rtype)

    def buildtransportxml(self, emane, type):
        ''' Write a transport XML file for the Virtual or Raw Transport.
        '''
        transdoc = emane.xmldoc("transport")
        trans = transdoc.getElementsByTagName("transport").pop()
        trans.setAttribute("name", "%s Transport" % type.capitalize())
        trans.setAttribute("library", "trans%s" % type.lower())
        trans.appendChild(emane.xmlparam(transdoc, "bitrate", "0"))

        flowcontrol = False
        names = self.model.getnames()
        values = emane.getconfig(self.objid, self.model._name,
                                 self.model.getdefaultvalues())[1]
        if "flowcontrolenable" in names and values:
            i = names.index("flowcontrolenable")
            if self.model.booltooffon(values[i]) == "on":
                flowcontrol = True

        if "virtual" in type.lower():
            if os.path.exists("/dev/net/tun_flowctl"):
                trans.appendChild(emane.xmlparam(transdoc, "devicepath",
                                  "/dev/net/tun_flowctl"))
            else:
                trans.appendChild(emane.xmlparam(transdoc, "devicepath",
                                  "/dev/net/tun"))
            if flowcontrol:
                trans.appendChild(emane.xmlparam(transdoc, "flowcontrolenable",
                                                 "on"))
        emane.xmlwrite(transdoc, self.transportxmlname(type.lower()))

    def transportxmlname(self, type):
        ''' Return the string name for the Transport XML file,
            e.g. 'n3transvirtual.xml'
        '''
        return "n%strans%s.xml" % (self.objid, type)


    def installnetifs(self, do_netns=True):
        ''' Install TAP devices into their namespaces. This is done after
            EMANE daemons have been started, because that is their only chance
            to bind to the TAPs.
        '''
        if not self.session.emane.doeventmonitor() and \
            self.session.emane.service is None:
            warntxt = "unable to publish EMANE events because the eventservice "
            warntxt += "Python bindings failed to load"
            self.session.exception(coreapi.CORE_EXCP_LEVEL_ERROR, self.name,
                                    self.objid, warntxt)

        for netif in self.netifs():
            if do_netns and "virtual" in netif.transport_type.lower():
                netif.install()
            netif.setaddrs()
            # if we are listening for EMANE events, don't generate them
            if self.session.emane.doeventmonitor():
                netif.poshook = None
                continue
            # at this point we register location handlers for generating
            # EMANE location events
            netif.poshook = self.setnemposition
            (x,y,z) = netif.node.position.get()
            self.setnemposition(netif, x, y, z)

    def deinstallnetifs(self):
        ''' Uninstall TAP devices. This invokes their shutdown method for
            any required cleanup; the device may be actually removed when
            emanetransportd terminates.
        '''
        for netif in self.netifs():
            if "virtual" in netif.transport_type.lower():
                netif.shutdown()
            netif.poshook = None

    def setnemposition(self, netif, x, y, z):
        ''' Publish a NEM location change event using the EMANE event service.
        '''
        if self.session.emane.service is None:
            if self.verbose:
                self.info("position service not available")
            return
        nemid =  self.getnemid(netif)
        ifname = netif.localname
        if nemid is None:
            self.info("nemid for %s is unknown" % ifname)
            return
        (lat, long, alt) = self.session.location.getgeo(x, y, z)
        if self.verbose:
            self.info("setnemposition %s (%s) x,y,z=(%d,%d,%s)"
                      "(%.6f,%.6f,%.6f)" % \
                      (ifname, nemid, x, y, z, lat, long, alt))
        if self.session.emane.version >= self.session.emane.EMANE091:
            event = LocationEvent()
        else:
            event = emaneeventlocation.EventLocation(1)
        # altitude must be an integer or warning is printed
        # unused: yaw, pitch, roll, azimuth, elevation, velocity
        alt = int(round(alt))
        if self.session.emane.version >= self.session.emane.EMANE091:
            event.append(nemid, latitude=lat, longitude=long, altitude=alt)
            self.session.emane.service.publish(0, event)
        else:
            event.set(0, nemid, lat, long, alt)
            self.session.emane.service.publish(emaneeventlocation.EVENT_ID,
                                           emaneeventservice.PLATFORMID_ANY,
                                           emaneeventservice.NEMID_ANY,
                                           emaneeventservice.COMPONENTID_ANY,
                                           event.export())

    def setnempositions(self, moved_netifs):
        ''' Several NEMs have moved, from e.g. a WaypointMobilityModel
            calculation. Generate an EMANE Location Event having several
            entries for each netif that has moved.
        '''
        if len(moved_netifs) == 0:
            return
        if self.session.emane.service is None:
            if self.verbose:
                self.info("position service not available")
            return

        if self.session.emane.version >= self.session.emane.EMANE091:
            event = LocationEvent()
        else:
            event = emaneeventlocation.EventLocation(len(moved_netifs))
        i = 0
        for netif in moved_netifs:
            nemid =  self.getnemid(netif)
            ifname = netif.localname
            if nemid is None:
                self.info("nemid for %s is unknown" % ifname)
                continue
            (x, y, z) = netif.node.getposition()
            (lat, long, alt) = self.session.location.getgeo(x, y, z)
            if self.verbose:
                self.info("setnempositions %d %s (%s) x,y,z=(%d,%d,%s)"
                          "(%.6f,%.6f,%.6f)" % \
                          (i, ifname, nemid, x, y, z, lat, long, alt))
            # altitude must be an integer or warning is printed
            alt = int(round(alt))
            if self.session.emane.version >= self.session.emane.EMANE091:
                event.append(nemid, latitude=lat, longitude=long, altitude=alt)
            else:
                event.set(i, nemid, lat, long, alt)
            i += 1

        if self.session.emane.version >= self.session.emane.EMANE091:
            self.session.emane.service.publish(0, event)
        else:
            self.session.emane.service.publish(emaneeventlocation.EVENT_ID,
                                           emaneeventservice.PLATFORMID_ANY,
                                           emaneeventservice.NEMID_ANY,
                                           emaneeventservice.COMPONENTID_ANY,
                                           event.export())


